
namespace Behemoth.SharePoint.Receivers {

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Xml;
    using System.IO;

    using Microsoft.SharePoint;

    using Behemoth.IO;
    using Behemoth.SharePoint.Logging;

    /// <summary>
    /// 
    /// </summary>
    /// <remarks>
    /// ReceiverAssembly="Behemoth.SharePoint.ARK, Version=1.?.?.?, Culture=neutral, PublicKeyToken=ecab56691def8148"
    /// ReceiverClass="Behemoth.SharePoint.Receivers.FixLookupFeatureReceiver"
    /// </remarks>
    public class FixLookupFeatureReceiver : Microsoft.SharePoint.SPFeatureReceiver {

        private SPSite site;
        private SPWeb rootWeb;
        private SPWeb parentWeb;
        private SPWeb referenceListWeb;
        private SPFeaturePropertyCollection xmlProperties;

        public FixLookupFeatureReceiver()
            : base() {
            //logWriter.
        }

        ~FixLookupFeatureReceiver() {
            try {
                if (this.site != null)
                    this.site.Dispose();
                if (this.rootWeb != null)
                    this.rootWeb.Dispose();
                if (this.parentWeb != null)
                    this.parentWeb.Dispose();
                if (this.referenceListWeb != null)
                    this.referenceListWeb.Dispose();
            } catch {
                // nothing much to do here really that we can think of
            } finally {
                this.site = null;
                this.rootWeb = null;
                this.parentWeb = null;
                this.referenceListWeb = null;
                this.xmlProperties = null;
            }
        }

        #region Custom Feature Proeprties

        // in this case targetWeb will hold the list that is being references, so it ends up being referencedWeb
        private string referenceListWebUrl {
            get { return GetXmlProperty("ReferenceListWeb", ""); }
        }

        /// <summary>
        /// The name of the list within lookupWeb that will be the target of the lokup sit ecolumn
        /// </summary>
        private string referenceListName {
            get { return GetXmlProperty("ReferenceListName", ""); }
        }

        /// <summary>
        /// The fieldName that will be used as the lookup fieldName in the target list
        /// </summary>
        private string lookupField {
            get { return GetXmlProperty("LookupField", "ID"); }
        }

        // TODO get this from the XML
        private string lookupFieldInternalName {
            get { return GetXmlProperty("LookupFieldInternalName", "Entity"); }
        }

        private string customFieldDefintiionXmlFilePath {
            get { return GetXmlProperty("CustomFieldDefintionXmlFilePath", ""); }
        }

        private string logPath {
            //get { return GetDictionaryProperty("LogPath", @"C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\LOGS\JHMMRPMasterPage"); }
            get { return GetXmlProperty("LogPath", @"C:\LogFile"); }
        }

        #endregion

        public override void FeatureInstalled(SPFeatureReceiverProperties properties) {
            //SetXmlProperties(properties);
            //return;
        }

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {
            //SetXmlProperties(properties);
            //return;
        }

        public override void FeatureActivated(SPFeatureReceiverProperties properties) {
            SetXmlProperties(properties);
            if (site == null)
                return;
            WriteLog("Invoking FeatureActivated(...).");
            try {
                // NOTE that the problem with this approach is that it results
                // in the site column being updated in the API and thus, ghosted
                // away from the definition in the Feature XML such that subsequent
                // changes to the definition may not propogate without removing its
                // definition from SharePoint's SQL tables.
                string sFinalCaml = GetFieldDefinitionXml();
                SPWeb siteColumnWeb = this.parentWeb == null ? this.rootWeb : this.parentWeb;
                CreateLookupColumn(siteColumnWeb, sFinalCaml, this.lookupFieldInternalName);
            } catch (Exception ex) {
                WriteLogException(ex);
            }
            WriteLog("FeatureActivated(...) Completed.");
        }


        public override void FeatureDeactivating
           (SPFeatureReceiverProperties properties) {
            SetXmlProperties(properties);
            if (site == null)
                return;
            WriteLog("Invoking FeatureDeactivating(...).");
            try {
                //ProcessWeb(rootWeb, false, true);
            } catch (Exception ex) {
                WriteLogException(ex);
            }
            WriteLog("FeatureDeactivating(...) Completed.");
        }

        #region Custom Property Reader

        

        private void SetXmlProperties(SPFeatureReceiverProperties properties) {
            bool doShowValues = false;
            if (xmlProperties == null) {
                doShowValues = true;
                xmlProperties = properties.Definition.Properties;
            }
            WriteLog("SetXmlProperties invoked.");
            if (!doShowValues) {
                WriteLog("  Properties already set. Exiting without action.");
                WriteLog("");
            }
            if (doShowValues) {
                WriteLog("Value assigned to xmlProperties.");
                //WriteLog("enableDeleteGalleryFilesOnDeactivation = " + enableDeleteGalleryFilesOnDeactivation.ToString());

                site = properties.Feature.Parent as SPSite;
                parentWeb = properties.Feature.Parent as SPWeb;
                if (site != null)
                    rootWeb = site.RootWeb;

                if (!string.IsNullOrEmpty(this.referenceListWebUrl)) {
                    try {
                        referenceListWeb = this.site.OpenWeb(this.referenceListWebUrl);
                    } catch (Exception ex) {
                        WriteLog(string.Format("Attempt to open reference list web at url '{0}' failed.", this.referenceListWebUrl));
                        WriteLogException(ex);
                    }
                }

                WriteLog("site exists = " + ((site != null).ToString()));
                WriteLog("parentWeb exists = " + ((parentWeb != null).ToString()));
                WriteLog("referenceListWeb exists = " + ((referenceListWeb != null).ToString()));
                WriteLog("rootWeb exists = " + ((rootWeb != null).ToString()));
                WriteLog("SetXmlProperties completed.");
                WriteLog("");
            }
        }

        #endregion
        #region Logging Support

        private BehemothLoggingService logWriter = new BehemothLoggingService();
        //private QuickLogWriter logWriter;

        private void EnsureLogWriter() {
            if (logWriter != null)
                return;
            /* try { */
            logWriter = new BehemothLoggingService(); //QuickLogWriter(logPath);
            /*
          } catch {
            logWriter = new QuickLogWriter(@"C:\EmergencyLog");
          }
             */
        }
        private void WriteLog(string text) {
            EnsureLogWriter();
            logWriter.Write(text);
        }
        private void WriteLogException(Exception ex) {
            EnsureLogWriter();
            logWriter.Write(ex);
            /*
            WriteLog("");
            WriteLog("*** Exception ***");
            WriteLog(ex.Message);
            WriteLog(ex.StackTrace);
            WriteLog("");
             */
        }

        private bool IsPropertySet(string propName) {
            return (!(xmlProperties == null || xmlProperties[propName] == null));
        }
        private string GetXmlProperty(string propName, string defaultValue) {
            if (!IsPropertySet(propName))
                return defaultValue;
            SPFeatureProperty prop = xmlProperties[propName];
            return prop.Value;
        }

        #endregion

        private string GetFieldDefinitionXml() {
            WriteLog("Invoking GetFieldDefinitionXml().");
            string sFieldElement = null;
            string filePath = this.customFieldDefintiionXmlFilePath;
            if (!File.Exists(filePath)) {
                throw new FileNotFoundException(string.Format("The custom field definition file that is part of feature '{0}' could not be found at the path '{1}' as specified in cusotm properties of the Feature.xml file.", "FWRReports", filePath));
            }
            XmlTextReader xReader = new XmlTextReader(filePath);
            // TODO make it possible to define multiple fields in this way and return the name attribute so we can create them appropriately in code
            while (xReader.Read()) {
                if (xReader.LocalName == "Field") {
                    sFieldElement = xReader.ReadOuterXml();
                    break;
                }
            }

            Guid referenceListId = GetReferenceListId();
            string sFinalCaml = ReplaceListGuidString(sFieldElement, referenceListId);
            WriteLog("Custom field definition XML: " + sFinalCaml);
            WriteLog("GetFieldDefinitionXml() completed.");
            return sFinalCaml;
        }

        /// <summary>
        /// Attempts to get the explicitly defined reference list web. If this is
        /// not defined, it will return the parent web if available, or the root web.
        /// </summary>
        /// <returns></returns>
        private SPWeb GetReferenceListWeb() {
            WriteLog("Invoking GetReferenceListWeb().");
            SPWeb listWeb = this.referenceListWeb;
            if (listWeb == null)
                listWeb = this.parentWeb == null ? this.rootWeb : this.parentWeb;
            if (listWeb == null) {
                string msg = "Critical error: could not open a web suitable to find list for referencing.";
                Exception ex = new Exception(msg);
                WriteLogException(ex);
                throw ex;
            }
            WriteLog("listWeb.Url = " + listWeb.Url);
            WriteLog("GetReferenceListWeb() completed.");
            return listWeb;
        }

        private Guid GetReferenceListId() {
            WriteLog("Invoking GetReferenceListId().");
            SPWeb listWeb = GetReferenceListWeb();
            // TODO determine and document is this the Name or the Title of the list???
            SPList list = null;
            try {
                list = this.referenceListWeb.Lists[this.referenceListName];
            } catch (Exception ex) {
                WriteLog(string.Format("Attempt to get reference list with name '{0}' in web '{1}' failed.", this.referenceListName, listWeb.Url));
                WriteLogException(ex);
                throw ex;
            }
            Guid referenceListId = list.ID;
            WriteLog("GetReferenceListId() completed.");
            return referenceListId;
        }

        private string ReplaceListGuidString(string sFieldElement, Guid referencedListId) {
            string sPopulatedGuid = string.Format("List=\"{0}\"", referencedListId);
            return sFieldElement.Replace("List=\"\"", sPopulatedGuid);
        }

        /// <summary>
        /// Attempt to delete the column. Note that this will fail if the column is inuse,
        /// i.e. it is used in a content type or list. I prefer to not catch the exception
        /// (though it may be useful to add extra logging), hence feature deactivation/re-
        /// activation will fail. This effectively means this feature cannot be deactivated whilst the column is in use.
        /// </summary>
        /// <param name="column">Column to delete.</param>
        private void TryColumnDelete(SPFieldLookup column) {
            WriteLog("Invoking TryColumnDelete(\"" + column.InternalName + "\").");
            try {
                column.Delete();
            } catch (SPException ex) {
                WriteLog("Attempt to delete site column with name '{0}' failed. There is likely a list or content type definition tha tis using this column. This may not be a critical error is the definition has not changed sinc ethe last time the feature was activated.");
                WriteLogException(ex);
                throw ex;
            }
            WriteLog("TryColumnDelete() completed.");
        }
        private void TryColumnDelete(SPWeb web, string sColumnName) {
            WriteLog("Invoking TryColumnDelete(\"" + sColumnName + "\").");
            SPFieldLookup lookupColumn = null;
            try {
                lookupColumn = web.Fields.GetFieldByInternalName(sColumnName) as SPFieldLookup;
            } catch (Exception /* ex */) {
                // fieldName is not found. Value does not fall within expected range.
                // TODO narrow this exception
            }
            if (lookupColumn != null)
                TryColumnDelete(lookupColumn);
            WriteLog("TryColumnDelete() completed.");
        }

        private void CreateLookupColumn(SPWeb web, string sColumnDefinitionXml, string sColumnName) {
            WriteLog("Invoking CreateLookupColumn(...).");
            // delete the column if it exists already and is not yet in use..
            TryColumnDelete(web, sColumnName);

            // now create the column from the CAML definition..
            string sCreatedColName = web.Fields.AddFieldAsXml(sColumnDefinitionXml);
            WriteLog("sCreatedColName = " + sCreatedColName);

            SPWeb listWeb = GetReferenceListWeb();
            if (listWeb == null)
                throw new Exception("Call to GetReferenceListWeb did not return a list.");
            // when these are equal, it's the same as the default / not defining the LookupWebId property
            if (this.parentWeb == null || this.parentWeb.ID != listWeb.ID) {
                SPFieldLookup lookupColumn = null;

                // also set LookupWebId so column can be used in webs other than web which hosts list..
                //lookupColumn = web.Fields[sCreatedColName] as SPFieldLookup;
                lookupColumn = web.Fields.GetFieldByInternalName(sCreatedColName) as SPFieldLookup;
                if (lookupColumn == null)
                    throw new Exception(string.Format("Attempt to get column with internal name '{0}' failed for newly creatred site column.", sCreatedColName));

                lookupColumn.LookupWebId = listWeb.ID;
                // TODO don't do this unconditionally until first we can prove that it is needed
                lookupColumn.Update();

                WriteLog("LookupList property is: " + lookupColumn.LookupList);
                WriteLog("Web ID is: " + lookupColumn.LookupWebId.ToString());
            }

            WriteLog("CreateLookupColumn(...) completed.");
        }

    } // class
} //namerspace

